#include <iostream>
#include <sstream>

#include <typeinfo>


#include <cppunit/TestCase.h>
#include <cppunit/TestFixture.h>
#include <cppunit/TestCaller.h>
#include <cppunit/TestResult.h>
#include <cppunit/TestSuite.h>

#include <cppunit/ui/text/TestRunner.h>

#include "rep/rep.hpp"
#include "rep/running.hpp"
#include "vm/opcodes.hpp"
#include "vm/builtins.hpp"
#include "vm/vm.hpp"


using namespace std;
using namespace rep;

namespace vm{
  class CallTest : public CppUnit::TestFixture{
  public:

    void setUp(){
      Builtin::initialize();
      //return five takes in no arguments, is defined with no parent
      //scope or parent type.  It returns five.
      ftype = FrameType(NULL, 16, NULL, NULL);

      two.kind = SIGNED;
      two.sig = 2;

      three.kind = SIGNED;
      three.sig = 3;

      five.kind = SIGNED;
      five.sig = 5;

      ret.kind = LOCAL;
      ret.parts.a = 0;
      ret.parts.b = Slot::RETURN;

      toPlaceFour.kind = LOCAL;
      toPlaceFour.parts.a = 0;
      toPlaceFour.parts.b = 4;
      toPlaceFive.kind = LOCAL;
      toPlaceFive.parts.a = 0;
      toPlaceFive.parts.b = 5;

    }
    void tearDown(){}
  protected:

    FrameType ftype;
    Ref two;
    Ref three;
    Ref five;
    Ref ret;

    Ref toPlaceFour;
    Ref toPlaceFive;
    
    void testDummy(){}

    void testSimpleFuncallBySteps(){
      Block code;
      Slot* env = FrameHelper::alloc(&ftype);
      Cons* st = NULL;
      Instruction pushFive (LOAD, 0, five);
      Instruction pushReturn (LOAD, 0, ret);
      Instruction doReturn (GO_CONT, 2, Ref());
      FrameType retFiveType = FrameType(NULL, 3, NULL, NULL);
      Block retFiveBlock;

      Lambda retFive (&retFiveType, &retFiveBlock);
      env[4].tag = Slot::LAMBDA;
      env[4].proc = &retFive;
      Ref toRetFive;
      toRetFive.kind = LOCAL;
      toRetFive.parts.a = 0;
      toRetFive.parts.b = 4;
      Instruction pushRetFive(LOAD, 0, toRetFive);
      Instruction callRetFive(CALL, 1, Ref());
      retFiveBlock.code.push_back(pushFive);
      retFiveBlock.code.push_back(pushReturn);
      retFiveBlock.code.push_back(doReturn);
      code.code.push_back(pushRetFive);
      code.code.push_back(callRetFive);
      code.code.push_back(BuiltinInstructions::print);
      Continuation cont (st, env, &code, 0);
      Continuation pushed = cont.next();
      CPPUNIT_ASSERT(pushed.stack);
      CPPUNIT_ASSERT(pushed.stack->car.proc == &retFive);
      Continuation called = pushed.next();
      Continuation fivePushed = called.next();
      Continuation returnPushed = fivePushed.next();
      Continuation returned = returnPushed.next();
      CPPUNIT_ASSERT(returned.stack);
      CPPUNIT_ASSERT_EQUAL(returned.stack->car.i, 5);
    }


    void testSimpleFuncallByRun(){
      Block code;
      Slot* env = FrameHelper::alloc(&ftype);
      Cons* st = NULL;

      Instruction pushFive (LOAD, 0, five);
      Instruction pushReturn (LOAD, 0, ret);
      Instruction doReturn (GO_CONT, 2, Ref());
      FrameType retFiveType = FrameType(NULL, 3, NULL, NULL);
      Block retFiveBlock;

      Lambda retFive (&retFiveType, &retFiveBlock);
      env[4].tag = Slot::LAMBDA;
      env[4].proc = &retFive;
      Ref toRetFive;
      toRetFive.kind = LOCAL;
      toRetFive.parts.a = 0;
      toRetFive.parts.b = 4;
      Instruction pushRetFive(LOAD, 0, toRetFive);
      Instruction callRetFive(CALL, 1, Ref());
      Instruction end(END, 1, Ref());
      retFiveBlock.code.push_back(pushFive);
      retFiveBlock.code.push_back(pushReturn);
      retFiveBlock.code.push_back(doReturn);
      code.code.push_back(pushRetFive);
      code.code.push_back(callRetFive);
      code.code.push_back(end);
      Continuation cont (st, env, &code, 0);
      Slot answer = run(cont);
      CPPUNIT_ASSERT_EQUAL(answer.i, 5);
    }

    void testFuncallWithArgs(){
      Block code;
      Slot* env = FrameHelper::alloc(&ftype);
      Cons* st = NULL;

      Instruction pushTwo (LOAD, 0, two);
      Instruction pushThree (LOAD, 0, three);
      Instruction pushReturn (LOAD, 0, ret);
      Instruction doReturn (GO_CONT, 2, Ref());
      int bindings[] = {4, 5};
      FrameType sumType = FrameType(env, 6, NULL, bindings);
      Block sumBlock;

      Lambda sum (&sumType, &sumBlock);
      env[4].tag = Slot::LAMBDA;
      env[4].proc = &sum;
      Ref toSum;
      toSum.kind = LOCAL;
      toSum.parts.a = 0;
      toSum.parts.b = 4;

      code.code.push_back(pushTwo);
      code.code.push_back(pushThree);
      Instruction pushSum(LOAD, 0, toSum);
      Instruction callSum(CALL, 3, Ref());
      Instruction end(END, 1, Ref());
      Instruction pushPlaceFour(LOAD, 0, toPlaceFour);
      Instruction pushPlaceFive(LOAD, 0, toPlaceFive);
      sumBlock.code.push_back(pushPlaceFour);
      sumBlock.code.push_back(pushPlaceFive);
      sumBlock.code.push_back(BuiltinInstructions::intAdd);
      sumBlock.code.push_back(pushReturn);
      sumBlock.code.push_back(doReturn);
      code.code.push_back(pushSum);
      code.code.push_back(callSum);
      code.code.push_back(end);
      Continuation cont (st, env, &code, 0);
      Slot answer = run(cont);
      CPPUNIT_ASSERT_EQUAL(answer.i, 5);
    }


  public:
    static CppUnit::Test* suite(){
      CppUnit::TestSuite *suite = new CppUnit::TestSuite("CallTest");

      suite->addTest( new CppUnit::TestCaller<CallTest>("testSimpleFuncallBySteps", 
                                                        &CallTest::testSimpleFuncallBySteps));
      suite->addTest( new CppUnit::TestCaller<CallTest>("testSimpleFuncallByRun", 
                                                        &CallTest::testSimpleFuncallByRun));
      suite->addTest( new CppUnit::TestCaller<CallTest>("testFuncallWithArgs", 
                                                        &CallTest::testFuncallWithArgs));
      return suite;
    }
  };
};
